<?php
abstract class ModelBase extends Object implements IDataModel
{
	protected $fields;
    protected $resourceId;
    protected $sortorderField = 'sort_order';
    protected $enabledField = 'enabled';
    protected $parentField;
    protected $primaryKeys;
    protected $oneToManyRelations = array();

    const FIELD_NEW_ID = 'FIELD_NEW_ID';

	public function ModelBase()
	{
		$this->LoadFields();
        $this->LoadPrimaryKeys();
	}

    protected function LoadPrimaryKeys()
    {
        $this->primaryKeys = array();
        foreach ($this->fields as $field)
        {
            if ($field->isPrimaryKey)
                $this->primaryKeys[] = $field->name;
        }
    }

    public function Find($condition = null, $fields = null, $orderby = null, $limit = null, &$page = null, &$count = null, $groupby = null)
    {}

    public function FindGroupby($groupby, $condition = null, $fields = null, $orderby = null, $limit = null, &$page = null, &$count = null)
    {
        return $this->Find($condition, $fields, $orderby, $limit, $page, $count, $groupby);
    }

    public function FindDefaultFields($condition = null, $orderby = null, $limit = null, &$page = null, &$count = null)
	{
		return $this->Find($condition, array_keys($this->fields), $orderby, $limit, $page, $count);
	}

	public function FindOne($condition = null, $fields = null, $orderby = null)
	{
		$items = $this->Find($condition, $fields, $orderby, 1);
		return empty($items) ? null : array_shift($items);
	}

	public function FindScalar($condition, $field, $orderby = null)
	{
		$item = $this->FindOne($condition, $field, $orderby);
        if (empty($item))
            return null;
        else
            return count($item) > 1 ? $item[$field] : array_shift($item);
	}

    public function FindById($id, $fields = null)
    {
    	return $this->FindOne($this->GetIdCondition($id), $fields);
    }

    public function FindScalarById($id, $field)
    {
        $item = $this->FindById($id, $field);
        return $item ? $item[$field] : null;
    }

    public function FindBy($field, $value, $fields = null, $orderby = null)
    {
        $cond = new SqlCondition();
        $cond->Add($field, $value, $this->fields[$field]->type);
        return $this->Find($cond, $fields, $orderby);
    }

    public function FindOneBy($field, $value, $fields = null, $orderby = null)
    {
        $cond = new SqlCondition();
        $cond->Add($field, $value, $this->fields[$field]->type);
        return $this->FindOne($cond, $fields, $orderby);
    }

    public function FindScalarBy($field, $value, $return_field, $orderby = null)
    {
        $cond = new SqlCondition();
        $cond->Add($field, $value, $this->fields[$field]->type);
        return $this->FindScalar($cond, $return_field, $orderby);
    }

    protected function IsUpdate(&$item)
    {
        $pk = $this->GetPrimaryKey();
        if (isset($item[$pk]))
        {
            return $item[$pk];
        }
        else
        {
            foreach ($this->primaryKeys as $field)
            {
                if (!isset($item[$field]))
                    return false;
                $ids[] = $item[$field];
            }
            $item[$pk] = implode('::', $ids);
            return $this->IsExisted($item[$pk]);
        }
    }

    public function IsExisted($id)
    {
        return $this->FindById($id) ? true : false;
    }

    public function GetIdByFieldValue($field, $value, $orderby = null)
    {
        $pk = $this->GetPrimaryKey();
        if (!$pk)
            throw new Exception('This model does not have primary key');

        $cond = new SqlCondition();
    	$cond->Add($field, $value, $this->fields[$field]->type);
    	return $this->FindScalar($cond, $pk, $orderby);
    }

    public function GetEmptyRecord()
    {
        $keys = array_keys($this->fields);
        foreach($keys as $k=>$v)
        {
            $items[$v] = null;
        }
        return $items;
    }

    public function Save($item)
    {
        return $this->IsUpdate($item) ? $this->Update($item) : $this->Insert($item);
    }

	abstract protected function LoadFields();

    public function GetPrimaryKey()
    {
        return empty($this->primaryKeys) ? null : implode('::', $this->primaryKeys);
    }

    protected function SetPrimaryKey($field_name)
    {
        $this->fields[$field_name]->isPrimaryKey = true;
    }

    public function GetForeignKeys()
    {
        $fks = array();
        foreach ($this->fields as $field_name => $field)
        {
            if ($field->foreignKey)
                $fks[$field_name] = $field->foreignKey;
        }
        return $fks;
    }

    protected function SetForeignKey($field, $ref_table, $ref_field)
    {
        $this->fields[$field]->SetForeignKey(new SqlForeignkey("FK_$field", $ref_table, $ref_field));
    }

    public function Del($id)
    {
        $this->CheckPermission('delete');
        foreach ($this->oneToManyRelations as $r)
        {
            $cond = new SqlCondition();
            $pk = $this->GetPrimaryKey();
            $cond->Add($r->GetRefField(), $id, $this->fields[$pk]->type);
            $r->GetModel()->Delete($cond);
        }
        return $this->DoDel($id);
    }

    public function Delete($condition)
    {
        $pk = $this->GetPrimaryKey();
        if (!$pk)
            throw new Exception('This model does not have primary key');

        $pks = explode('::', $pk);

        $items = $this->Find($condition, $pks);
        foreach ($items as $item)
        {
            $v = array();
            foreach ($pks as $k)
            {
                $v[] = $item[$k];
            }
            $this->Del(implode('::', $v));
        }
    }

    protected function AddOneToManyRelation($model, $refField)
    {
        $this->oneToManyRelations[] = new OneToManyRelation($model, $refField);
    }

	public function Update($item)
	{
        if (empty($item)) return false;
        $this->ValidateBeforeUpdate($item);
        $this->UpdateSortOrder($item);
        return $this->DoUpdate($item);
    }

    protected function ValidateBeforeUpdate(&$item)
    {
        $this->CheckPermission('edit');

        if (isset($item['resource_id']))
        {
            $pk = $this->GetPrimaryKey();
            $current_rsrc = $this->FindScalarById($item[$pk], 'resource_id');
            $user_id = AppConfig::GetInstance()->GetUserModel()->GetCurrentUserId();
            AclPermissionModel::Authorize($user_id, $current_rsrc, 'edit');
        }

        // auto insert datemodified
        if (!isset($item['datemodified']) && isset($this->fields['datemodified']))
            $item['datemodified'] = DatetimeBase::Now();

        // auto insert modifiedby
        if (!isset($item['modifiedby']) && isset($this->fields['modifiedby']))
        {
            $user_id = $this->GetUserId();
            if ($user_id) $item['modifiedby'] = $user_id;
        }
    }

    public function UpdateCondition($item, $condition)
    {
        if (!$condition)
            throw new Exception('Update condition is null');

        $pk = $this->GetPrimaryKey();
        if (!$pk)
            throw new Exception('This model does not have primary key');

        $updates = $this->Find($condition, $pk);
        foreach ($updates as $update)
        {
            $item[$pk] = $update[$pk];
            $this->Update($item);
        }
    }

	public function Insert($item)
	{
        if (empty($item)) return false;
        $this->ValidateBeforeInsert($item);
        $this->SetSortOrder($item);
        return $this->DoInsert($item);
    }

    protected function ValidateBeforeInsert(&$item)
    {
        $this->CheckPermission('add');

        // auto insert datecreated
        if (!isset($item['datecreated']) && isset($this->fields['datecreated']))
            $item['datecreated'] = DatetimeBase::Now();

        // auto insert createdby
        if (!isset($item['createdby']) && isset($this->fields['createdby']))
        {
            $user_id = $this->GetUserId();
            if ($user_id) $item['createdby'] = $user_id;
        }
    }

    protected function GetUserId()
    {
        return AppConfig::GetInstance()->GetUserModel()->GetCurrentUserId();
    }

    protected function DoInsert()
    {}

    protected function DoUpdate()
    {}

    protected function DoDelete()
    {}

    public static function GetInstanceByName($table_name)
    {
        $class_name = String::Pascalize($table_name);
        return call_user_func("{$class_name}Model::GetInstance");
    }

    protected function GetIdCondition($id)
    {
        $pk = $this->GetPrimaryKey();
        $pks = explode('::', $pk);
        $ids = explode('::', $id);
        $table = $this->GetTableName();
        $cond = new SqlCondition();
        if (count($pks) == 1)
        {
            $fieldName = $table ? "$table.$pk" : $pk;
            $cond->Add($fieldName, $id, $this->fields[$pk]->type);
            return $cond;
        }
        else
        {
            foreach ($pks as $i => $p)
            {
                $key = $ids[$i];
                $fieldName = $table ? "$table.$p" : "`$p`";
                $cond->Add($fieldName, $key, $this->fields[$p]->type);
            }
            return $cond;
        }
    }

    public function ListBy($field, $value, $keyfield, $orderby = null)
    {
        $items = $this->FindBy($field, $value, $keyfield, $orderby);
        return Set::Pluck($items, $keyfield);
    }

    public function ListField($field, $condition = null, $orderby = null)
    {
        $items = $this->Find($condition, $field, $orderby);
        return Set::Pluck($items, $field);
    }

    public function InsertMultiple($items)
    {
        foreach ($items as $item)
        {
            $this->Insert($item);
        }
    }

    public function GetFilter($key)
    {
        $filter = RequestHandler::GetFilter();
        return isset($filter[$key]) ? $filter[$key] : null;
    }

    public function GetPhpExport($rows = null)
    {
        if (!is_array($rows))
            $rows = $this->Find();

        $out[] = '// '.implode(', ', array_keys($this->fields));
        foreach ($rows as $row)
        {
            $out[] = $this->GetPhpRowExport($row);
        }

        $content = implode("\n", $out);
        return "<?php\n$content\n?>";
    }

    protected function GetPhpRowExport($row)
    {
        $values = $this->GetCsvRowExport($row, ', ', 'null', "'");
        return "\$this->AddRow($values);";
    }

    public function GetFieldsString()
    {
        return implode(', ', array_keys($this->fields));
    }

    protected function GetNewId($item)
    {
        return isset($item[self::FIELD_NEW_ID]) ? $item[self::FIELD_NEW_ID] : null;
    }

    protected function SetNewId(&$item, $value)
    {
        $item[self::FIELD_NEW_ID] = $value;
    }

    protected function AddField($name, $type = 'varchar', $nullable = true, $is_primarykey = false, $default = null, $auto_increment = null, $charset = null, $collation = null, $comment = null)
    {
        $field = $this->fields[$name] = new ModelField($name, $type, $nullable, $is_primarykey, $default, $auto_increment, $this->GetTableName(), $charset, $collation, $comment);
        $field->position = count($this->fields);
        return $field;
    }

    public function GetField($field_name)
    {
        if (!isset($this->fields[$field_name]))
            throw new Exception("Field '$field_name' not exists");

        return $this->fields[$field_name];
    }

    protected function AddFields($fields)
    {
        $this->fields = array_merge($this->fields, $fields);
    }

    public function GetFields()
    {
        return $this->fields;
    }

    public function SetField($field)
    {
        $this->fields[$field->name] = $field;
    }

    public function SetFields($fields)
    {
        $this->fields = $fields;
    }

    protected function GetDataFile()
    {
        return AppConfig::GetInstance()->DIR_DATA.DS."{$this->GetLowercaseName()}.db.php";
    }

    public function ExportPhp()
    {
        return FileHandler::Write($this->GetDataFile(), $this->GetPhpExport());
    }

    public function GetLowercaseName()
    {
        return String::CropEnd('_model', parent::GetLowercaseName());
    }

    public function GetTableName()
    {
        return $this->GetLowercaseName();
    }

    protected function IsEmpty($field_name, $item)
    {
        return !isset($item[$field_name]) || empty($item[$field_name]);
    }

    protected function CheckPermission($action)
    {
        if (!$this->resourceId) return;
        $user_id = AppConfig::GetInstance()->GetUserModel()->GetCurrentUserId();
        AclPermissionModel::Authorize($user_id, $this->resourceId, $action);
    }

    public function ExportCsv($delimiter = null, $file = null)
    {
        if (!$file) $file = $this->GetDataFile();
        return FileHandler::Write($file, $this->GetCsvExport($delimiter));
    }

    public function GetCsvExport($delimiter = null, $rows = null)
    {
        if (!$delimiter) $delimiter = ', ';
        if (!is_array($rows))
            $rows = $this->Find();

        $out[] = implode($delimiter, array_keys($this->fields));
        foreach ($rows as $row)
        {
            $out[] = $this->GetCsvRowExport($row, $delimiter);
        }
        return implode("\n", $out);
    }

    protected function GetCsvRowExport($row, $delimiter, $nullValue = null, $quote = null)
    {
        foreach($row as $fieldname => &$value)
        {
            if (!array_key_exists($fieldname, $this->fields))
            {
                unset($row[$fieldname]);
                continue;
            }

            if (is_null($value)) $value = $nullValue;
            else if (!is_numeric($value))
            {
                if ($quote) $value = addslashes($value);
                $value = $quote.$value.$quote;
            }
        }
        return implode($delimiter, $row);
    }

    protected function SetSortOrder(&$item)
    {
        if ($this->sortorderField && $this->parentField && !isset($item[$this->sortorderField]))
        {
            $parentId = isset($item[$this->parentField]) ? $item[$this->parentField] : null;
            $item[$this->sortorderField] = $this->FindScalarBy($this->parentField, $parentId, $this->sortorderField, "{$this->sortorderField} DESC")+1;
        }
    }

    protected function UpdateSortOrder(&$item)
    {
        if ($this->sortorderField && $this->parentField && isset($item[$this->sortorderField]))
        {
            $pk = $this->GetPrimaryKey();
            $id = $this->GetPrimaryKeyValue($item);
            $i = $this->FindById($id, "$pk, {$this->parentField}");
            $parent_id = $i[$this->parentField];
            $i[$this->sortorderField] = $item[$this->sortorderField];

            $items = $this->FindBy($this->parentField, $parent_id, "$pk, {$this->sortorderField}");
            $items = Set::ReOrder($i, $items, $pk, $this->sortorderField);
            foreach ($items as $it)
            {
                $this->DoUpdate($it);
            }
            unset($item[$this->sortorderField]);
        }
    }

    protected function GetPrimaryKeyValue(&$item)
    {
        $pk = $this->GetPrimaryKey();
        return $item[$pk];
    }

    public function GetEnabled($fields = null, $orderby = null)
    {
        return $this->FindBy($this->enabledField, 1, $fields, $orderby);
    }

    public function GetEnabledOrdered($fields = null)
    {
        return $this->GetEnabled($fields, $this->sortorderField);
    }
}
?>